<template>
  <transition-group
    class="h-full w-full"
    v-bind="$attrs"
    ref="elRef"
    :name="transitionName"
    :tag="tag"
    mode="out-in"
  >
    <div key="component" v-if="isInit">
      <slot :loading="loading"></slot>
    </div>
    <div key="skeleton" v-else>
      <slot name="skeleton" v-if="$slots.skeleton"></slot>
      <Skeleton v-else />
    </div>
  </transition-group>
</template>
<script lang="ts">
import type { PropType } from "vue";
import { defineComponent, onMounted, reactive, ref, toRef, toRefs } from "vue";
import { Skeleton } from "ant-design-vue";
import { useIntersectionObserver, useTimeoutFn } from "@vueuse/core";

interface State {
  isInit: boolean;
  loading: boolean;
  intersectionObserverInstance: IntersectionObserver | null;
}

const props = {
  /**
   * Waiting time, if the time is specified, whether visible or not, it will be automatically loaded after the specified time
   */
  timeout: { type: Number },
  /**
   * The viewport where the component is located.
   * If the component is scrolling in the page container, the viewport is the container
   */
  viewport: {
    type: (typeof window !== "undefined" ? window.HTMLElement : Object) as PropType<HTMLElement>,
    default: () => null
  },
  /**
   * Preload threshold, css unit
   */
  threshold: { type: String, default: "0px" },
  /**
   * The scroll direction of the viewport, vertical represents the vertical direction, horizontal represents the horizontal direction
   */
  direction: {
    type: String,
    default: "vertical",
    validator: (v) => ["vertical", "horizontal"].includes(v)
  },
  /**
   * The label name of the outer container that wraps the component
   */
  tag: { type: String, default: "div" },
  maxWaitingTime: { type: Number, default: 80 },
  /**
   * transition name
   */
  transitionName: { type: String, default: "lazy-container" }
};

export default defineComponent({
  name: "LazyContainer",
  components: { Skeleton },
  inheritAttrs: false,
  props,
  emits: ["init"],
  setup(props, { emit }) {
    const elRef = ref();
    const state = reactive<State>({
      isInit: false,
      loading: false,
      intersectionObserverInstance: null
    });

    onMounted(() => {
      immediateInit();
      initIntersectionObserver();
    });

    // If there is a set delay time, it will be executed immediately
    function immediateInit() {
      const { timeout } = props;
      timeout &&
      useTimeoutFn(() => {
        init();
      }, timeout);
    }

    function init() {
      state.loading = true;

      useTimeoutFn(() => {
        if (state.isInit) return;
        state.isInit = true;
        emit("init");
      }, props.maxWaitingTime || 80);
    }

    function initIntersectionObserver() {
      const { timeout, direction, threshold } = props;
      if (timeout) return;
      // According to the scrolling direction to construct the viewport margin, used to load in advance
      let rootMargin = "0px";
      switch (direction) {
        case "vertical":
          rootMargin = `${threshold} 0px`;
          break;
        case "horizontal":
          rootMargin = `0px ${threshold}`;
          break;
      }

      try {
        const { stop, observer } = useIntersectionObserver({
          rootMargin,
          target: toRef(elRef.value, "$el"),
          onIntersect: (entries: any[]) => {
            const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio;
            if (isIntersecting) {
              init();
              if (observer) {
                stop();
              }
            }
          },
          root: toRef(props, "viewport")
        });
      } catch (e) {
        init();
      }
    }

    return {
      elRef,
      ...toRefs(state)
    };
  }
});
</script>
